package de.justphil.websocketclient;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import java.util.regex.Pattern;

import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;




public class WebSocketConnection implements WebSocket {
	
	public static final int SOCKET_TIMEOUT = 20000;
	
	private URI url = null;
	private WebSocketListener webSocketListener = null;
	
	private volatile boolean connected = false;
	
	private Socket socket = null;
	private InputStream input = null;
	private BufferedOutputStream output = null;
	
	private WebSocketReceiver receiver = null;
	private WebSocketHandshake handshake = null;
	
	private Random rand;
	
	
	public WebSocketConnection(URI url) throws WebSocketException {
		this(url, null);
	}
	

	public WebSocketConnection(URI url, String protocol) throws WebSocketException {
		this.url = url;
		this.rand = new Random(System.currentTimeMillis());
		handshake = new WebSocketHandshake(url, protocol);
	}
	

	public void setWebSocketListener(WebSocketListener webSocketListener) {
		if (webSocketListener == null) {
			throw new IllegalArgumentException("webSocketListener is null");
		}
		
		this.webSocketListener = webSocketListener;
	}
	
	
	public WebSocketListener getWebSocketListener() {
		return webSocketListener;
	}
	

	public synchronized void connect() throws WebSocketException {
		try {
			if (connected) {
				throw new WebSocketException("already connected");
			}
			
			socket = createSocket();
			input = socket.getInputStream();
			output = new BufferedOutputStream(socket.getOutputStream(), 8192);
			
			output.write(handshake.getHandshake());
			output.flush();
						
			//boolean handshakeComplete = false;
			//boolean header = true;
			//int len = 8192;
			//byte[] buffer = new byte[len];
			//int pos = 0;
			List<String> handshakeLines = new ArrayList<String>();
			
			//byte[] serverResponse = new byte[16];
			
			//Log.i("WebSocketConnection", "READING SERVER HANDSHAKE...");
			
			BufferedReader bufferedIn = new BufferedReader(new InputStreamReader(input, "ISO-8859-1"));
			
			String s = null;
			boolean ready = false;
			while( (s = bufferedIn.readLine()) != null && !ready ) {
				//Log.i("WebSocketConnection", s);
				handshakeLines.add(s);
				ready = checkReady(s);
			}
			
			//Log.i("WebSocketConnection", "Lines => " + handshakeLines.size());
			
			if (handshakeLines.size() == 0) {
				throw new WebSocketException("connection couldn't be established (android bug)");
			}
			
			//Log.i("WebSocketConnection", "READING SERVER HANDSHAKE...DONE!");
			
//			Log.i("WebSocketConnection", "VERIFYING SERVER HANDSHAKE...");
			handshake.verifyServerStatusLine(handshakeLines.get(0));
			
			handshake.verifyServerResponse(handshakeLines.get(3));
			
			handshakeLines.remove(0);
			
			HashMap<String, String> headers = new HashMap<String, String>();
			for (String line : handshakeLines) {
				String[] keyValue = line.split( Pattern.quote( ": " ) );
				headers.put(keyValue[0].trim(), keyValue[1].trim());
			}
			handshake.verifyServerHandshakeHeaders(headers);
			
//			Log.i("WebSocketConnection", "CREATING RECEIVER");
			receiver = new WebSocketReceiver(input, this);
			receiver.start();
			connected = true;
			
			if (webSocketListener == null) {
				throw new WebSocketException("webSocketListener is null");
			}
			else {
				webSocketListener.onConnect();
			}
		}
		catch (WebSocketException wse) {
			throw wse;
		}
		catch (IOException ioe) {
			throw new WebSocketException("error while connecting: " + ioe.getMessage(), ioe);
		}
	}
	
	public boolean isConnected() {
		return connected;
	}
	

	public synchronized void send(String data) throws WebSocketException {
		if (!connected) {
			throw new WebSocketException("error while sending text data: not connected");
		}
		
		try {
			//0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 = masked "Hello"
			
			/*
			output.write(0x81); // fin (1), rsv1 (1), rsv2 (1), rsv3 (1) and op code (4)
			output.write(0x85); // mask (1) and payload len (7)
			output.write(0x37); // *
			output.write(0xfa); // * masking key (32)
			output.write(0x21); // *
			output.write(0x3d); // *
			output.write(0x7f); // #
			output.write(0x9f); // #
			output.write(0x4d); // # payload text data, UTF-8 encoded (payload len)
			output.write(0x51); // #
			output.write(0x58); // #
			
			output.flush();
			*/
			
			if (data == null)
				return;
			
			data = data.trim();
			
			if (data.length() == 0)
				return;
			
			
			// FIN bit set / rsv1/2/3 set to 0 / op code %x1 denotes a text frame
			output.write(0x81); // fin (1), rsv1 (1), rsv2 (1), rsv3 (1) and op code (4)
			
			
			/*
			 * 	The length of the "Payload data", in bytes: if 0-125, that is the
				payload length.  If 126, the following 2 bytes interpreted as a
				16-bit unsigned integer are the payload length.  If 127, the
				following 8 bytes interpreted as a 64-bit unsigned integer (the
				most significant bit MUST be 0) are the payload length.  Multibyte
				length quantities are expressed in network byte order.  Note that
				in all cases, the minimal number of bytes MUST be used to encode
				the length, for example, the length of a 124-byte-long string
				can't be encoded as the sequence 126, 0, 124.  The payload length
				is the length of the "Extension data" + the length of the
				"Application data".  The length of the "Extension data" may be
				zero, in which case the payload length is the length of the
				"Application data".
			 */

			byte[] payloadBytes = data.getBytes("UTF-8");
			long payloadSize = payloadBytes.length;
			
			int i = 0;
			if (payloadSize <= 125) {
//				Log.i("WebSocketConnection", "payloadSize <= 125");
//				Log.i("WebSocketConnection", "payloadSize == " + payloadSize);
				
				i = (int) payloadSize;
				i = i | 128; // set mask bit to 1
				
//				Log.i("WebSocketConnection", "On the wire: " + i);
				
				output.write(i); // mask (1) and payload len (7)
			}
			else if (payloadSize <= 65535) { // 65535 == biggest possible 16 bit value
//				Log.i("WebSocketConnection", "payloadSize <= 65535");
				
				i = 126;
				i = i | 128; // set mask bit to 1
				output.write(i); // mask (1) and payload len (7)
				
				// 16-bit unsigned integer (2 bytes)
				byte[] bit16 = ByteBuffer.allocate(4).putInt( (int) payloadSize ).array();
				output.write(bit16, 2, 2); // send the two low order bytes
			}
			else if (payloadSize <= Long.MAX_VALUE) {
//				Log.i("WebSocketConnection", "payloadSize <= Long.MAX_VALUE");
				
				i = 127;
				i = i | 128; // set mask bit to 1
				output.write(i); // mask (1) and payload len (7)
				
				// Java's 64-bit signed integer (long) has most significant bit set to 0 for positive values
				byte[] bit64 = ByteBuffer.allocate(8).putLong( payloadSize ).array();
				output.write(bit64);
			}
			else {
				throw new WebSocketException("error while sending text data: message is too long");
			}
			
			/*
			 * 
			 * j                   = i MOD 4
     		 * transformed-octet-i = original-octet-i XOR masking-key-octet-j
			 * 
			 */
			
			byte[] maskingKey = new byte[] { (byte) getRandomInt(), (byte) getRandomInt(), (byte) getRandomInt(), (byte) getRandomInt() };
			
			output.write(maskingKey[0]); // *
			output.write(maskingKey[1]); // * masking key (32)
			output.write(maskingKey[2]); // *
			output.write(maskingKey[3]); // *
			
			
			output.write(maskPayload(payloadBytes, maskingKey)); // UTF-8 encoded payload data
			
			output.flush();
			
			/*
			output.write(0x00);
			output.write(data.getBytes(("UTF-8")));
			output.write(0xff);
			output.flush();
			*/
		}
		catch (UnsupportedEncodingException uee) {
			throw new WebSocketException("error while sending text data: unsupported encoding", uee);
		}
		catch (IOException ioe) {
			throw new WebSocketException("error while sending text data", ioe);
		}
	}

	
	public void handleReceiverError() {
		try {
			if (connected) {
				close();
			}
		}
		catch (WebSocketException wse) {
			wse.printStackTrace();
		}
	}
	

	public synchronized void close() throws WebSocketException {
		try {
			if (!connected) {
				return;
			}
			
			sendCloseHandshake();
		}
		catch (WebSocketException wse) {
			throw wse;
		}
		finally {
			try {
				closeStreams();
			}
			catch (WebSocketException ignored) {
				
			}
			finally {
				if (receiver != null && receiver.isRunning()) {
					receiver.stopIt();
				}
				
				if (webSocketListener == null) {
					throw new WebSocketException("webSocketListener is null");
				}
				else {
					webSocketListener.onClose();
				}
			}
			
		}
	}
	
	public synchronized void closeSilently() {
		try {
			close();
		} catch (WebSocketException e) {
			// ignored
		}
	}
	
	
	private synchronized void sendCloseHandshake() throws WebSocketException {
		if (!connected) {
			throw new WebSocketException("error while sending closing handshake: not connected");
		}
		
		try {
			// fin (1), rsv1 (1), rsv2 (1), rsv3 (1), and op code (4)
			output.write(0x88);
			
			// mask bit (1) and payload length (7)
			int i = 2; // payload length will be 2 bytes (Close Code Number)
			i = i | 128; // set mask bit to 1
			output.write(i);
			
			
			byte[] maskingKey = new byte[] { (byte) getRandomInt(), (byte) getRandomInt(), (byte) getRandomInt(), (byte) getRandomInt() };
			output.write(maskingKey[0]); // *
			output.write(maskingKey[1]); // * masking key (32)
			output.write(maskingKey[2]); // *
			output.write(maskingKey[3]); // *
			
			// masked payload (Close Code Number encoded by 2 bytes)
			byte[] bit16 = ByteBuffer.allocate(4).putInt( 1000 ).array();
			
			byte[] payload = new byte[2];
			System.arraycopy(bit16, 2, payload, 0, 2);
			
			output.write(maskPayload(payload, maskingKey)); // send the two low order bytes indicating a normal closure
			
			output.flush();
		}
		catch (IOException ioe) {
			throw new WebSocketException("error while sending closing handshake", ioe);
		}
		finally{
			connected = false;
		}
	}
	

	private Socket createSocket() throws WebSocketException {
		String scheme = url.getScheme();
		String host = url.getHost();
		int port = url.getPort();
		
		Socket socket = null;
		
		if (scheme != null && scheme.equals("ws")) {
			if (port == -1) {
				port = 80;
			}
			try {
				SocketAddress sockAddr = new InetSocketAddress(host, port);
				socket = new Socket();
				socket.connect(sockAddr, SOCKET_TIMEOUT);
			}
			catch (SocketTimeoutException ste) {
				throw new WebSocketException("socket timeout while connecting to " + host);
			}
			catch (UnknownHostException uhe) {
				throw new WebSocketException("unknown host: " + host, uhe);
			}
			catch (IOException ioe) {
				throw new WebSocketException("error while creating socket to " + url, ioe);
			}
		}
		else if (scheme != null && scheme.equals("wss")) {
			if (port == -1) {
				port = 443;
			}
			try {
				SocketFactory factory = SSLSocketFactory.getDefault();
				socket = factory.createSocket(host, port);
			}
			catch (UnknownHostException uhe) {
				throw new WebSocketException("unknown host: " + host, uhe);
			}
			catch (IOException ioe) {
				throw new WebSocketException("error while creating secure socket to " + url, ioe);
			}
		}
		else {
			throw new WebSocketException("unsupported protocol: " + scheme);
		}
		
		return socket;
	}
	
	
	private void closeStreams() throws WebSocketException {
		try {
			if (input != null) {
				input.close();
			}
			
			if (output != null) {
				output.close();
			}
			
			if (socket != null) {
				socket.close();
			}
		}
		catch (IOException ioe) {
			throw new WebSocketException("error while closing websocket connection: ", ioe);
		}
	}
	
	private boolean checkReady(String s) {
		boolean ready = false;
		
		if (handshake.getProtocol() != null && s.trim().startsWith("Sec-WebSocket-Protocol")) {
			ready = true;
		}
		else if (s.trim().startsWith("Sec-WebSocket-Accept")) {
			ready = true;
		}
		
		return ready;
	}
	
	private int getRandomInt() {
		return rand.nextInt(256);
	}
	
	private byte[] maskPayload(byte[] payload, byte[] maskingKey) {
		byte[] out = new byte[payload.length];
		
		for(int i = 0; i < out.length; i++) {
			out[i] = (byte) (payload[i] ^ maskingKey[i%4]);
		}
		
		return out;
	}
}
